/*
 * GStandings.java
 * Created on 18 juin 2007
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2009, Jens und Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.data.dao.standings;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Locale;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.GAbstractDao;
import com.hockeo.vhbase.data.dao.IGEntity;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.match.GMatch;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.helper.GMatchResult;
import com.hockeo.vhbase.data.helper.GMatchType;
import com.hockeo.vhbase.data.helper.GStandingsRankTendency;
import com.hockeo.vhbase.data.helper.GStandingsRankType;
import com.hockeo.vhbase.data.helper.GStandingsType;

/**
 * Represents a single league standings object.
 *
 * @version $Id$
 * @author jjanke
 */
@Entity
@Table( name = "s_standings" )
public class GStandings extends GAbstractDao
{
  @Id
  @Column( name = "id" )
  @GeneratedValue( strategy = GenerationType.AUTO )
  private int                    d_nId;

  @Enumerated( EnumType.STRING )
  @Column( name = "type" )
  private GStandingsType         d_type;

  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "club_id" )
  private GClub                  d_club;

  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "matchday_id" )
  private GMatchday              d_matchday;

  @Column( name = "rank" )
  private int                    d_nRank               = 1;

  @Column( name = "rank_prev" )
  private int                    d_nPrevRank           = 1;

  @Column( name = "rank_cross_element" )
  private int                    d_nCrossElementRank   = 0;

  @Enumerated( EnumType.STRING )
  @Column( name = "rank_tendency" )
  private GStandingsRankTendency d_rankTendency        = GStandingsRankTendency.UNCHANGED;

  @Enumerated( EnumType.STRING )
  @Column( name = "rank_type" )
  private GStandingsRankType     d_rankType            = GStandingsRankType.MIDFIELD;

  @Column( name = "matches" )
  private int                    d_nNumMatches         = 0;

  @Column( name = "points" )
  private int                    d_nNumPoints          = 0;

  @Column( name = "points_pct" )
  private BigDecimal             d_decPointsPercentage = new BigDecimal( "1.000" );

  @Column( name = "points_avg" )
  private BigDecimal             d_decPointsAverage    = new BigDecimal( "0.000" );

  @Column( name = "wins" )
  private int                    d_nNumWins            = 0;

  @Column( name = "wins_ot" )
  private int                    d_nNumWinsOT          = 0;

  @Column( name = "losses_ot" )
  private int                    d_nNumLossesOT        = 0;

  @Column( name = "losses" )
  private int                    d_nNumLosses          = 0;

  @Column( name = "goals_for" )
  private int                    d_nNumGoalsFor        = 0;

  @Column( name = "goals_against" )
  private int                    d_nNumGoalsAgainst    = 0;

  @Column( name = "goals_diff" )
  private int                    d_nGoalsDiff          = 0;

  @Column( name = "goals_account" )
  private BigDecimal             d_decRemainingGoals   = GRulesConstants.ZERO_GOALS;

  /** Default parameterless constructor. */
  protected GStandings()
  {}

  /**
   * Creates a new empty standings objects.
   *
   * @param type the standings type
   * @param club the club for which the standings are
   * @param matchday the matchday the standings are attached to
   */
  public GStandings( GStandingsType type, GClub club, GMatchday matchday, BigDecimal decRemainingGoals )
  {
    d_type = type;
    d_club = club;
    d_matchday = matchday;
    d_decRemainingGoals = decRemainingGoals;
  }

  /**
   * Creates a new standings object for the given matchday based on the given previous
   * standings object and the played match. The kind of standings that is created depends
   * on the type of the previous standings object.
   *
   * @param prevStandings the standings of the previous matchday
   * @param matchday the matchday to which the new standings are attached
   * @param match the match which is used to update the previous standings
   */
  public GStandings( GStandings prevStandings, GMatchday matchday, GMatch match )
  {
    this( prevStandings, matchday );

    update( match );
  }

  /**
   * Creates a new standings object which corresponds almost completely to the given
   * previous standings object. Only the matchday is adjusted.
   *
   * @param prevStandings the standings of the previous matchday
   * @param matchday the matchday to which the new standings are attached
   */
  public GStandings( GStandings prevStandings, GMatchday matchday )
  {
    d_type = prevStandings.getType();
    d_club = prevStandings.getClub();
    d_matchday = matchday;
    d_nPrevRank = prevStandings.getRank();
    d_decRemainingGoals = prevStandings.getRemainingGoals();

    d_nNumMatches = prevStandings.getNumMatches();
    d_nNumPoints = prevStandings.getNumPoints();
    d_nNumWins = prevStandings.getNumWins();
    d_nNumWinsOT = prevStandings.getNumWinsOT();
    d_nNumLossesOT = prevStandings.getNumLossesOT();
    d_nNumLosses = prevStandings.getNumLosses();
    d_nNumGoalsFor = prevStandings.getNumGoalsFor();
    d_nNumGoalsAgainst = prevStandings.getNumGoalsAgainst();
    d_nGoalsDiff = prevStandings.getGoalsDiff();
    d_decPointsAverage = prevStandings.getPointsPerMatchAvg();
    d_decPointsPercentage = prevStandings.getPointsPercentage();
  }

  /**
   * Updates these standings with the result of the given match.
   *
   * @param match the match which is used to update the standings
   */
  public void update( GMatch match )
  {
    final GMatchType matchType = match.getMatchType( d_club );
    final GMatchResult matchResult = match.getResult( d_club );

    // Remaining goals are updated in any case
    //
    d_decRemainingGoals = d_decRemainingGoals.subtract( match.getCost( d_club ) );

    if ( d_type == GStandingsType.TOTAL || d_type.isCompatible( matchType ) )
    {
      d_nNumMatches++;
      d_nNumPoints += matchResult.getNumPoints();
      d_nNumGoalsFor += match.getNumGoals( d_club );
      d_nNumGoalsAgainst += match.getNumGoalsOpponent( d_club );
      d_nGoalsDiff = d_nNumGoalsFor - d_nNumGoalsAgainst;
      d_decPointsAverage = BigDecimal.valueOf( d_nNumPoints ).divide( BigDecimal.valueOf( d_nNumMatches ), 3,
          RoundingMode.HALF_UP );
      d_decPointsPercentage = BigDecimal.valueOf( d_nNumPoints ).divide(
          BigDecimal.valueOf( d_nNumMatches * GMatchResult.VICTORY.getNumPoints() ), 3, RoundingMode.HALF_UP );

      switch ( matchResult )
      {
        case LOSS:
          d_nNumLosses++;
          break;

        case LOSS_AFTER_SHOOTOUT:
          d_nNumLossesOT++;
          break;

        case VICTORY:
          d_nNumWins++;
          break;

        case VICTORY_AFTER_SHOOTOUT:
          d_nNumWinsOT++;
          break;

        default:
          throw new IllegalStateException( "Math result " + matchResult.name() + " not supported." );
      }
    }
  }

  /**
   * Resets this standings object by resetting all values to 0 and the goal account to the
   * same value as the passed in as argument.
   *
   * @param decGoals the goal account to use as initial value for the remaining goals
   */
  public void reset( final BigDecimal decGoals )
  {
    d_nRank = 1;
    d_nPrevRank = 1;
    d_rankTendency = GStandingsRankTendency.UNCHANGED;
    d_rankType = GStandingsRankType.MIDFIELD;
    d_nNumMatches = 0;
    d_nNumPoints = 0;
    d_decPointsPercentage = new BigDecimal( "1.000" );
    d_decPointsAverage = new BigDecimal( "0.000" );
    d_nNumWins = 0;
    d_nNumWinsOT = 0;
    d_nNumLossesOT = 0;
    d_nNumLosses = 0;
    d_nNumGoalsFor = 0;
    d_nNumGoalsAgainst = 0;
    d_nGoalsDiff = 0;
    d_decRemainingGoals = decGoals;
  }

  /**
   * Sets the rank of this standings object and updates the improvement tendency if these
   * standings are attributed to a matchday having a number higher than 1.
   *
   * @param nRank the rank to be attributed to these standings
   */
  public void setRank( int nRank )
  {
    d_nRank = nRank;

    // There is no tendency to update if it's the first matchday
    //
    if ( d_matchday.getNum() > 1 )
    {
      if ( d_nRank < d_nPrevRank )
        d_rankTendency = GStandingsRankTendency.IMPROVED;
      else if ( d_nRank > d_nPrevRank )
        d_rankTendency = GStandingsRankTendency.DEGRADED;
      else
        d_rankTendency = GStandingsRankTendency.UNCHANGED;
    }
  }

  /**
   * @see #getCrossElementRank()
   */
  public void setCrossElementRank( int nRank )
  {
    d_nCrossElementRank = nRank;
  }

  public GStandingsType getType()
  {
    return d_type;
  }

  public GClub getClub()
  {
    return d_club;
  }

  public GMatchday getMatchday()
  {
    return d_matchday;
  }

  public int getRank()
  {
    return d_nRank;
  }

  public int getPreviousRank()
  {
    return d_nPrevRank;
  }

  /**
   * When the standings belongs to a contest element which in turn belongs to a round
   * having multiple round robin contest elements attached to it, this method returns the
   * overall rank.
   */
  public int getCrossElementRank()
  {
    return d_nCrossElementRank;
  }

  public GStandingsRankTendency getRankTendency()
  {
    return d_rankTendency;
  }

  public int getNumMatches()
  {
    return d_nNumMatches;
  }

  public int getNumPoints()
  {
    return d_nNumPoints;
  }

  public int getNumWins()
  {
    return d_nNumWins;
  }

  public int getNumWinsOT()
  {
    return d_nNumWinsOT;
  }

  public int getNumLossesOT()
  {
    return d_nNumLossesOT;
  }

  public int getNumLosses()
  {
    return d_nNumLosses;
  }

  public int getNumGoalsFor()
  {
    return d_nNumGoalsFor;
  }

  public int getNumGoalsAgainst()
  {
    return d_nNumGoalsAgainst;
  }

  public int getGoalsDiff()
  {
    return d_nGoalsDiff;
  }

  public BigDecimal getRemainingGoals()
  {
    return d_decRemainingGoals;
  }

  /**
   * Sets rank type which can e.g. be used to determine the layout of the standings line
   * when rendered as HTML.
   *
   * @param rankType the rank type to be set
   */
  public void setRankType( GStandingsRankType rankType )
  {
    d_rankType = rankType;
  }

  /**
   * Returns the rank type which can e.g. be used to determine the layout of the standings
   * line when rendered as HTML.
   */
  public GStandingsRankType getRankType()
  {
    return d_rankType;
  }

  public String getRankType( Locale locale )
  {
    return translate( "rankType_" + d_rankType.name(), locale );
  }

  /**
   * Returns the number of matches still remaining to be played by the club for which this
   * standings object is intended for. The returned number depends on the type of the
   * standings. If it is a total standings objects, the total number of remaining matches
   * is returned, otherwise the remaining number of home or away matches is given. The
   * returned value only returns the number of remaining matches based on this standings
   * object. If there are more recent standings objects (because more matchdays have been
   * played), this method may not return the intended value.
   *
   * @return the total number of remaining matches based on the number of matches being
   *         played until this standings object was created
   */
  public int getNumRemainingMatches()
  {
    switch ( d_type )
    {
      case TOTAL:
        return d_matchday.getContestElement().getNumMatchesPerClub() - d_nNumMatches;

      case HOME:
        return d_matchday.getContestElement().getNumMatchesPerClub() / 2 - d_nNumMatches;

      case AWAY:
        return d_matchday.getContestElement().getNumMatchesPerClub() / 2 - d_nNumMatches;

      default:
        throw new IllegalStateException( "The StandingsType " + d_type.name() + " is not supported by the method." );
    }
  }

  /**
   * Returns the avg. number of goals scored per match. If no match has been played so
   * far, 0 is returned.
   */
  public double getGoalsPerMatchAvg()
  {
    return d_nNumMatches > 0 ? d_nNumGoalsFor / (double) d_nNumMatches : 0;
  }

  /**
   * Returns the avg. number of goals scored by the opponents per match. If no match has
   * been played so far, 0 is returned.
   */
  public double getGoalsPerMatchAvgOpponents()
  {
    return d_nNumMatches > 0 ? d_nNumGoalsAgainst / (double) d_nNumMatches : 0;
  }

  /**
   * Returns the avg. number of points per match. If no match has been played so far, 0 is
   * returned.
   */
  public BigDecimal getPointsPerMatchAvg()
  {
    return d_decPointsAverage;
  }

  /**
   * Returns the number of points as a fraction of the maximum possible number of points.
   * The returned value lies between 0 and 1. If no match has been played, 1 is returned.
   */
  public BigDecimal getPointsPercentage()
  {
    return d_decPointsPercentage;
  }

  public int getId()
  {
    return d_nId;
  }

  public Class<? extends IGEntity> getGEntityClass()
  {
    return GStandings.class;
  }

  /*
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals( Object obj )
  {
    try
    {
      return d_nId == ( (GStandings) obj ).getId();
    }
    catch ( Exception ex ) // especially ClassCastException(s) may happen
    {
      return false;
    }
  }

  /*
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode()
  {
    return d_nId;
  }

  /*
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString()
  {
    return "Standings " + d_nId + " for " + d_club.getName() + " (contest element: "
        + d_matchday.getContestElement().getId() + ")";
  }
}
